/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.services;

import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.text.SimpleDateFormat;
import org.internna.iwebjtracker.model.TrackerUser;
import org.internna.iwebjtracker.issue.model.Issue;
import org.internna.iwebjtracker.model.DefaultProject;
import org.internna.iwebjtracker.model.statistics.OpenClosed;
import org.internna.iwebmvc.dao.DAO;
import org.internna.iwebmvc.model.User;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.security.UserManager;
import org.internna.iwebmvc.utils.CollectionUtils;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Transactional;

/**
 * Generates activity reports using a request parameter.
 *
 * @author Jose Noheda
 * @since 1.0
 */
@Transactional
public final class RequestActivityReporter implements ActivityReporter {

    private DAO dao;
    private UserManager userManager;
    private TrackerUserManager trackerUserManager;

    private static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH);

    /**
     * Spring required injection point.
     *
     * @param dao a non null instance
     */
    @Required public void setDao(DAO dao) {
        Assert.notNull(dao);
        this.dao = dao;
    }

    /**
     * Spring required injection point.
     *
     * @param userManager a non null instance
     */
    @Required public void setUserManager(UserManager userManager) {
        Assert.notNull(userManager);
        this.userManager = userManager;
    }

    /**
     * Spring required injection point.
     *
     * @param trackerUserManager a non null instance
     */
    @Required public void setTrackerUserManager(TrackerUserManager trackerUserManager) {
        Assert.notNull(trackerUserManager);
        this.trackerUserManager = trackerUserManager;
    }

    /**
     * Obtain the activity data filtered by permissions of the current user grouped by activity type.
     *
     * @return
     */
    @Transactional(readOnly = true)
    @Override public List<Object[]> getRecentActivityByType() {
        Map.Entry<String, Map<String, Object>> security = getSecurityClause().entrySet().iterator().next();
        return (List) dao.executeQuery("SELECT a.type, COUNT(a.type) FROM Activity a WHERE " + security.getKey() + " GROUP BY a.type", 0, 100, security.getValue());
    }

    /**
     * Obtain the activity data filtered by permissions of the current user grouped by activity priority.
     *
     * @return a group of type of activity and a number.
     */
    @Transactional(readOnly = true)
    @Override public List<Object[]> getRecentActivityByPriority() {
        Map.Entry<String, Map<String, Object>> security = getSecurityClause().entrySet().iterator().next();
        return (List) dao.executeQuery("SELECT a.activityPriority, COUNT(a.activityPriority) FROM Activity a WHERE " + security.getKey() + " GROUP BY a.activityPriority", 0, 100, security.getValue());
    }

    private Map<String, Map<String, Object>> getSecurityClause() {
        Map<String, Object> securityClause = null;
        Map<String, Map<String, Object>> clauses = new HashMap<String, Map<String, Object>>(1);
        String clause = "( ( a.publicView = true )";
        User user = userManager.getActiveUser();
        if ((user != null) && (!user.isAnonymous())) {
            clause += " OR ( :owner IN ELEMENTS(a.viewers) )";
            securityClause = new HashMap<String, Object>(1);
            securityClause.put("owner", user);
        }
        clause += " )";
        clauses.put(clause, securityClause);
        return clauses;
    }

    /**
     * Generates user statistics using SQL queries.
     *
     * @return a map with two keys (TOTAL_USERS, TOTAL_DEVELOPERS, TOTAL_DISABLED_USERS)
     */
    @Override public Map<String, Object> getUsersStatistics() {
        Map<String, Object> statistics = new HashMap<String, Object>();
        statistics.put(TOTAL_USERS, dao.executeQuery("SELECT COUNT(username) FROM UserImpl").get(0));
        statistics.put(TOTAL_DISABLED_USERS, dao.executeQuery("SELECT COUNT(username) FROM UserImpl WHERE enabled = false").get(0));
        return statistics;
    }

    /**
     * Generates project statistics using SQL queries.
     *
     * @return a map with three keys (TOTAL_ACTIVITY, TOTAL_PROJECTS, TOTAL_DEVELOPERS)
     */
    @Override public Map<String, Object> getProjectsStatistics() {
        Map<String, Object> statistics = new HashMap<String, Object>();
        statistics.put(TOTAL_ACTIVITY, dao.executeQuery("SELECT COUNT(id) FROM Activity").get(0));
        statistics.put(TOTAL_PROJECTS, dao.executeQuery("SELECT COUNT(id) FROM DefaultProject").get(0));
        Map<String, Object> status = new HashMap<String, Object>(1);
        status.put("status", Issue.Status.CLOSED);
        statistics.put(TOTAL_OPEN_ISSUES, dao.executeQuery("SELECT COUNT(id) FROM DefaultIssue WHERE status != :status", 0, 99999, status).get(0));
        status.put("blocker", Issue.Severity.BLOCKER);
        status.put("critical", Issue.Severity.CRITICAL);
        statistics.put(TOTAL_CRITICAL_OPEN_ISSUES, dao.executeQuery("SELECT COUNT(id) FROM DefaultIssue WHERE status != :status AND (severity = :critical OR severity = :blocker)", 0, 99999, status).get(0));
        status.clear();
        status.put("assigned", Issue.Status.ASSIGNED);
        status.put("wip", Issue.Status.WORK_IN_PROGRESS);
        statistics.put(TOTAL_ASSIGNED, dao.executeQuery("SELECT COUNT(id) FROM DefaultIssue WHERE status = :assigned OR status = :wip)", 0, 99999, status).get(0));
        statistics.put(TOTAL_DEVELOPERS, dao.executeNativeQuery("SELECT COUNT(DISTINCT developer) FROM PROJECTS_DEVELOPERS").get(0));
        return statistics;
    }

    /**
     * Generates project statistics for the user in session using SQL queries.
     *
     * @return a map with three keys (TOTAL_ACTIVITY, TOTAL_PROJECTS, TOTAL_DEVELOPERS)
     */
    @Override public Map<String, Object> getProjectsStatisticsForCurrentUser() {
        Map<String, Object> statistics = new HashMap<String, Object>();
        TrackerUser trackerUser = trackerUserManager.getCurrentTrackerUser();
        if (trackerUser != null) {
            Map<String, Object> params = new HashMap<String, Object>(1);
            params.put("user", trackerUser);
            statistics.put(TOTAL_PROJECTS, dao.executeNativeQuery("SELECT COUNT(observer) FROM PROJECTS_OBSERVERS WHERE observer = :user", 0, 100, params).get(0));
            statistics.put(TOTAL_DEVELOPERS, dao.executeNativeQuery("SELECT COUNT(developer) FROM PROJECTS_DEVELOPERS WHERE developer = :user", 0, 100, params).get(0));
        }
        return statistics;
    }

    /**
     * Retrieves the information available from the last month about open and closed issues by project.
     *
     * @return any
     */
    @SuppressWarnings("unchecked")
    @Override public Map<String, String> getOpenClosedIssuesForCurrentUserProjects() {
        TrackerUser user = trackerUserManager.getCurrentTrackerUser();
        if (user != null) {
            Set<DefaultProject> projects = new HashSet<DefaultProject>(user.getMemberships());
            projects.addAll(user.getTrackings());
            if (CollectionUtils.isNotEmpty(projects)) {
                return obtainOpenClosedIssuesFor(projects);
            }
        }
        return null;
    }

    private Map<String, String> obtainOpenClosedIssuesFor(Set<DefaultProject> projects) {
        Map<String, String> chartData = null;
        Map<String, Object> params = new HashMap<String, Object>(1);
        params.put("projects", projects);
        List data = dao.findByQuery("SELECT e FROM OpenClosed e WHERE e.project IN (:projects) ORDER BY e.date DESC", 0, 30, params);
        if (CollectionUtils.isNotEmpty(data)) {
            Collections.reverse(data);
            data = mergeByDate(data);
            chartData = new HashMap<String, String>();
            chartData.put("cht", "lc");
            chartData.put("chxt", "x,y");
            chartData.put("chxl", "0:|" + getXLabels(data));
            chartData.put("chxp", "0,18,83");
            int max = getRoundedMaxValue(data);
            chartData.put("chxr", "1,0," + max);
            chartData.put("chd", "t:" + getData(data, max, true) + "|" + getData(data, max, false));
        }
        return chartData;
    }

    /**
     * Merges all the elements from different projects into one item per day collection.
     *
     * @param data a non empty collection
     * @returna non empty collection whose size is less or equal than the provided
     */
    protected List<OpenClosed> mergeByDate(List<OpenClosed> data) {
        List<OpenClosed> parsed = new ArrayList<OpenClosed>();
        OpenClosed old = null;
        for (OpenClosed openClosed : data) {
            List<OpenClosed> merged = openClosed.merge(old);
            old = merged.get(0);
            if (merged.size() > 1) {
                parsed.add(old);
                old = merged.get(1);
            }
        }
        parsed.add(old);
        return parsed;
    }

    /**
     * Parses the first and last dates from the list.
     *
     * @param data a non null non empty list
     * @return a non empty string like 2002/10/01|2009/10/23
     */
    private String getXLabels(List<OpenClosed> data) {
        return dateFormatter.format(data.get(0).getDate()) + "|" + dateFormatter.format(data.get(data.size() - 1).getDate());
    }

    /**
     * Gets the max value of all the open and closed from the list (rounded to the nearest 10).
     *
     * @param data a non null non empty collection
     * @return a positive integer
     */
    protected int getRoundedMaxValue(Collection<OpenClosed> data) {
        int max = 1;
        for (OpenClosed openClosed : data) {
            max = Math.max(max, Math.max(openClosed.getClosed(), openClosed.getOpen()));
        }
        return max % 10 == 0  ? max : ((max / 10) + 1) * 10;
    }

    /**
     * Claculates the percentage that the second parameter represents.
     *
     * @param max a positive integer
     * @param number a positive integer less than max
     * @return a positive integer between 0 and 100
     */
    protected String percentage(int max, int number) {
        return String.valueOf(100 * number / max);
    }

    /**
     * Converts open or closed data into a comma separated string of percentages.
     *
     * @param data a non null non empty collection
     * @param max a positive integer
     * @param open any
     * @return something like 20,40,60
     */
    protected String getData(List<OpenClosed> data, int max, boolean open) {
        StringBuilder values = new StringBuilder();
        for (OpenClosed openClosed : data) {
            values.append(percentage(max, open ? openClosed.getOpen() : openClosed.getClosed())).append(",");
        }
        return values.deleteCharAt(values.length() - 1).toString();
    }

}
